package com.lazi.june.gateway.filter;

import com.lazi.june.common.base.RequestConst;
import com.lazi.june.gateway.config.GatewayProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.InetSocketAddress;
import java.time.Instant;
import java.util.concurrent.TimeUnit;

/**
 * IP 黑名单过滤器
 * <p>
 * 基于访问频率的智能黑名单系统，防止恶意攻击和爬虫。
 * 在指定时间窗口内，如果某个 IP 的访问次数超过阈值，将自动加入黑名单，禁止访问一段时间。
 * <p>
 * 核心功能：
 * 1. 实时统计每个 IP 在时间窗口内的访问次数
 * 2. 超过阈值自动加入黑名单
 * 3. 黑名单 IP 在封禁时间内无法访问
 * 4. 支持白名单 IP（永不封禁）
 * 5. 使用 Redisson 存储，支持分布式部署
 * <p>
 * 工作原理：
 * <pre>
 * 1. 时间窗口统计（滑动窗口）
 *    - 使用 Redisson 的 RAtomicLong 记录每个 IP 的访问计数
 *    - 计数器过期时间 = 时间窗口大小
 *    - 每次访问计数 +1
 *
 * 2. 黑名单触发
 *    - 如果访问次数 > 阈值，加入黑名单
 *    - 黑名单记录包含封禁到期时间
 *
 * 3. 黑名单检查
 *    - 每次请求先检查是否在黑名单中
 *    - 如果在黑名单且未到期，返回 403
 *    - 如果已到期，Redisson 会自动移除
 * </pre>
 * <p>
 * 配置示例：
 * <pre>
 * june:
 *   gateway:
 *     blacklist:
 *       enabled: true
 *       window-seconds: 60          # 时间窗口：1分钟
 *       threshold: 100              # 阈值：100次
 *       ban-seconds: 1800           # 封禁时长：30分钟
 *       whitelist-ips:
 *         - 127.0.0.1               # 本地开发
 *         - 192.168.1.100           # 内网IP
 * </pre>
 * <p>
 * 使用场景：
 * - 防止 DDoS 攻击
 * - 限制爬虫频率
 * - 防止暴力破解
 * - 保护敏感接口
 * <p>
 * 注意事项：
 * - 使用 Redisson 实现分布式黑名单
 * - 白名单 IP 不受任何限制
 * - 建议根据实际业务调整阈值和时间窗口
 *
 * @author huliqian
 * @since 2025/1/15
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class BlacklistFilter implements GlobalFilter, Ordered {

    /**
     * Redisson 客户端（用于分布式存储访问统计和黑名单）
     */
    private final RedissonClient redissonClient;

    /**
     * 网关配置属性
     */
    private final GatewayProperties props;

    /**
     * Redis Key 前缀：访问计数
     */
    private static final String KEY_PREFIX_ACCESS_COUNT = "gateway:blacklist:count:";

    /**
     * Redis Key 前缀：黑名单
     */
    private static final String KEY_PREFIX_BLACKLIST = "gateway:blacklist:ban:";

    /**
     * 过滤器核心逻辑：黑名单检查和统计
     * <p>
     * 执行流程：
     * 1. 检查是否启用黑名单功能
     * 2. 解析客户端 IP 地址
     * 3. 检查是否在白名单中（白名单直接放行）
     * 4. 检查是否在黑名单中（黑名单直接拒绝）
     * 5. 统计访问次数
     * 6. 判断是否超过阈值，超过则加入黑名单
     * 7. 继续过滤器链
     *
     * @param exchange 当前请求的 ServerWebExchange
     * @param chain    过滤器链
     * @return Mono<Void> 异步响应
     */
    @NonNull
    @Override
    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull GatewayFilterChain chain) {
        // 1. 检查是否启用黑名单功能
        GatewayProperties.BlacklistConfig config = props.getBlacklist();
        if (!config.isEnabled()) {
            return chain.filter(exchange);
        }

        // 2. 解析客户端 IP
        String clientIp = resolveClientIp(exchange.getRequest());

        // 3. 检查是否在白名单中（白名单直接放行）
        if (isWhitelisted(clientIp)) {
            return chain.filter(exchange);
        }

        // 4. 检查是否在黑名单中
        if (isBlacklisted(clientIp)) {
            log.warn("[Blacklist] IP {} is in blacklist, request denied", clientIp);
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }

        // 5. 统计访问次数
        long accessCount = incrementAccessCount(clientIp);

        // 6. 判断是否超过阈值
        if (accessCount > config.getThreshold()) {
            addToBlacklist(clientIp);
            log.warn("[Blacklist] IP {} exceeded threshold ({} > {}), added to blacklist for {} seconds",
                    clientIp, accessCount, config.getThreshold(), config.getBanSeconds());
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }

        // 7. 继续过滤器链
        return chain.filter(exchange);
    }

    /**
     * 解析客户端 IP 地址
     * <p>
     * 识别优先级：
     * 1. X-Forwarded-For 请求头（支持代理、负载均衡场景）
     * 2. 请求的远程 IP 地址
     *
     * @param request HTTP 请求
     * @return 客户端 IP 地址
     */
    private String resolveClientIp(ServerHttpRequest request) {
        // 1. 优先使用 X-Forwarded-For
        String xff = request.getHeaders().getFirst(RequestConst.FORWARDED_FOR);
        if (xff != null && !xff.isBlank()) {
            // X-Forwarded-For 可能包含多个 IP，取第一个（真实客户端 IP）
            return xff.split(",")[0].trim();
        }

        // 2. 使用远程 IP
        InetSocketAddress addr = request.getRemoteAddress();
        if (addr != null && addr.getAddress() != null) {
            return addr.getAddress().getHostAddress();
        }

        return "unknown";
    }

    /**
     * 检查 IP 是否在白名单中
     *
     * @param ip IP 地址
     * @return 是否在白名单中
     */
    private boolean isWhitelisted(String ip) {
        return props.getBlacklist().getWhitelistIps().contains(ip);
    }

    /**
     * 检查 IP 是否在黑名单中
     * <p>
     * 从 Redis 中读取黑名单记录
     * Redisson 会自动处理过期，无需手动检查
     *
     * @param ip IP 地址
     * @return 是否在黑名单中
     */
    private boolean isBlacklisted(String ip) {
        String key = KEY_PREFIX_BLACKLIST + ip;
        RBucket<Long> bucket = redissonClient.getBucket(key);
        return bucket.isExists();
    }

    /**
     * 增加访问计数
     * <p>
     * 使用 Redisson 的 RAtomicLong 记录时间窗口内的访问次数
     * 原子操作保证线程安全和分布式一致性
     *
     * @param ip IP 地址
     * @return 当前访问次数
     */
    private long incrementAccessCount(String ip) {
        String key = KEY_PREFIX_ACCESS_COUNT + ip;
        RAtomicLong counter = redissonClient.getAtomicLong(key);

        // 增加计数
        long count = counter.incrementAndGet();

        // 如果是第一次访问，设置过期时间
        if (count == 1) {
            counter.expire(props.getBlacklist().getWindowSeconds(), TimeUnit.SECONDS);
        }

        return count;
    }

    /**
     * 将 IP 加入黑名单
     * <p>
     * 使用 Redisson 的 RBucket 存储黑名单记录，并设置过期时间
     *
     * @param ip IP 地址
     */
    private void addToBlacklist(String ip) {
        GatewayProperties.BlacklistConfig config = props.getBlacklist();

        // 添加到黑名单
        String blacklistKey = KEY_PREFIX_BLACKLIST + ip;
        RBucket<Long> bucket = redissonClient.getBucket(blacklistKey);
        long expireTime = Instant.now().getEpochSecond() + config.getBanSeconds();
        bucket.set(expireTime, config.getBanSeconds(), TimeUnit.SECONDS);

        // 清除访问计数
        String countKey = KEY_PREFIX_ACCESS_COUNT + ip;
        redissonClient.getAtomicLong(countKey).deleteAsync();
    }

    /**
     * 过滤器执行顺序
     * <p>
     * 返回 -110，在认证之前、CORS 之后执行
     * <p>
     * 执行顺序参考：
     * - CorsFilter: -200 (CORS 跨域)
     * - BlacklistFilter: -110 (黑名单检查)
     * - JwtAuthFilter: -100 (认证)
     * - RequestBodyCacheFilter: -90 (请求体缓存)
     * - RateLimitFilter: -88 (限流)
     * - RequestLoggingFilter: -80 (日志)
     * <p>
     * 黑名单检查应该在认证之前，避免浪费资源处理黑名单 IP 的请求
     *
     * @return 顺序值，越小越早执行
     */
    @Override
    public int getOrder() {
        // 在认证之前、CORS 之后执行
        return -110;
    }
}